#include "OpenGLBuffers.h"

#include "OpenGLBase.h"

namespace SE
{
	//---------------------------------------------------------------------------------------------------------------------------
	// VertexBuffer
	//---------------------------------------------------------------------------------------------------------------------------

	OpenGLVertexBuffer::OpenGLVertexBuffer(const U32 size, void* buffer) : mRenderID(0), mSize(size)
	{
		SE_PROFILE_FUNCTION();

		SE_ASSERT(size >= 0, "Buffer size out of range!");

		GLCALL(glCreateBuffers(1, &mRenderID));
		GLCALL(glBindBuffer(GL_ARRAY_BUFFER, mRenderID));
		GLCALL(glBufferData(GL_ARRAY_BUFFER, size, buffer, GL_STATIC_DRAW));
	}

	OpenGLVertexBuffer::~OpenGLVertexBuffer()
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glDeleteBuffers(1, &mRenderID));
	}

	void OpenGLVertexBuffer::bind() const
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glBindBuffer(GL_ARRAY_BUFFER, mRenderID));
	}

	void OpenGLVertexBuffer::unbind() const
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glBindBuffer(GL_ARRAY_BUFFER, 0));
	}

	void OpenGLVertexBuffer::setBuffer(void* buffer, const U32 size)
	{
		SE_PROFILE_FUNCTION();

		SE_ASSERT(size >= 0 && size <= mSize, "Buffer size out of range!");

		GLCALL(glBindBuffer(GL_ARRAY_BUFFER, mRenderID));
		GLCALL(glBufferSubData(GL_ARRAY_BUFFER, 0, size, buffer));
	}

	const BufferLayout& OpenGLVertexBuffer::getLayout() const
	{
		return mLayout;
	}

	void OpenGLVertexBuffer::setLayout(const BufferLayout& layout)
	{
		mLayout = layout;
	}

	//---------------------------------------------------------------------------------------------------------------------------
	// IndexBuffer
	//---------------------------------------------------------------------------------------------------------------------------

	OpenGLIndexBuffer::OpenGLIndexBuffer(const U32 count, void* buffer) : mRenderID(0), mCount(count)
	{
		SE_PROFILE_FUNCTION();
		
		SE_ASSERT(count > 0, "Buffer size out of range!");

		// GL_ELEMENT_ARRAY_BUFFER is not valid without an actively bound VAO
		// Binding with GL_ARRAY_BUFFER allows the data to be loaded regardless of VAO state. 
		GLCALL(glCreateBuffers(1, &mRenderID));
		GLCALL(glBindBuffer(GL_ARRAY_BUFFER, mRenderID));
		GLCALL(glBufferData(GL_ARRAY_BUFFER, mCount * sizeof(U32), buffer, GL_STATIC_DRAW));
	}

	OpenGLIndexBuffer::~OpenGLIndexBuffer()
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glDeleteBuffers(1, &mRenderID));
	}

	void OpenGLIndexBuffer::bind() const
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mRenderID));
	}

	void OpenGLIndexBuffer::unbind() const
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));
	}

	void OpenGLIndexBuffer::setBuffer(void* buffer, const U32 size)
	{
		SE_PROFILE_FUNCTION();

		SE_ASSERT(size >= 0 && size <= mCount * sizeof(U32), "Buffer size out of range!");

		GLCALL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mRenderID));
		GLCALL(glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, size, buffer));
	}
    
	U32 OpenGLIndexBuffer::getCount() const
    {
        return mCount;
    }

	//---------------------------------------------------------------------------------------------------------------------------
	// VertexArray
	//---------------------------------------------------------------------------------------------------------------------------

	OpenGLVertexArray::OpenGLVertexArray() : mRenderID(0)
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glCreateVertexArrays(1, &mRenderID));
	}

	OpenGLVertexArray::~OpenGLVertexArray()
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glDeleteVertexArrays(1, &mRenderID));
	}

	void OpenGLVertexArray::bind() const
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glBindVertexArray(mRenderID));
	}

	void OpenGLVertexArray::unbind() const
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glBindVertexArray(0));
	}

	void OpenGLVertexArray::addVertexBuffer(const Ref<VertexBuffer>& vertexBuffer)
	{
		SE_PROFILE_FUNCTION();

		SE_ASSERT(vertexBuffer->getLayout().getElements().size(), "Vertex Buffer has no layout!");

		GLCALL(glBindVertexArray(mRenderID));
		vertexBuffer->bind();

		const auto& layout = vertexBuffer->getLayout();
		for (const auto& element : layout)
		{
			switch (element.type)
			{
				default: SE_ASSERT(false, "Unknown ShaderDataType!"); break;
				case ShaderDataType::Float:
				case ShaderDataType::Float2:
				case ShaderDataType::Float3:
				case ShaderDataType::Float4:
				{
					GLCALL(glEnableVertexAttribArray(mVertexBufferIndex));
					GLCALL(glVertexAttribPointer(
						mVertexBufferIndex,
						element.getComponentCount(),
						ToGLenum(element.type),
						element.normalized ? GL_TRUE : GL_FALSE,
						layout.getStride(),
						(const void*)(element.offset)));
					mVertexBufferIndex++;
					break;
				}
				case ShaderDataType::Int:
				case ShaderDataType::Int2:
				case ShaderDataType::Int3:
				case ShaderDataType::Int4:
				case ShaderDataType::Bool:
				{
					GLCALL(glEnableVertexAttribArray(mVertexBufferIndex));
					GLCALL(glVertexAttribIPointer(
						mVertexBufferIndex,
						element.getComponentCount(),
						ToGLenum(element.type),
						layout.getStride(),
						(const void*)(element.offset)));
					mVertexBufferIndex++;
					break;
				}
				case ShaderDataType::Mat3:
				case ShaderDataType::Mat4:
				{
					U8 count = element.getComponentCount();
					for (U8 i = 0; i < count; i++)
					{
						GLCALL(glEnableVertexAttribArray(mVertexBufferIndex));
						GLCALL(glVertexAttribPointer(
							mVertexBufferIndex,
							count,
							ToGLenum(element.type),
							element.normalized ? GL_TRUE : GL_FALSE,
							layout.getStride(),
							(const void*)(element.offset + sizeof(Float) * count * i)));
						mVertexBufferIndex++;
					}
					break;
				}
			}
		}
		mVertexBuffers.push_back(vertexBuffer);
	}

	void OpenGLVertexArray::setIndexBuffer(const Ref<IndexBuffer>& indexBuffer)
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glBindVertexArray(mRenderID));
		indexBuffer->bind();

		mIndexBuffer = indexBuffer;
	}
    
	const Vector< Ref<VertexBuffer> >& OpenGLVertexArray::getVertexBuffers() const
    {
        return mVertexBuffers;
    }
    
	const Ref<IndexBuffer>& OpenGLVertexArray::getIndexBuffer() const
    {
        return mIndexBuffer;
    }

	//---------------------------------------------------------------------------------------------------------------------------
	// TextureBuffer 
	//---------------------------------------------------------------------------------------------------------------------------

	static inline GLenum ToGLTextureTarget(const U32 dimension, const Boolean multisample)
	{
		if (1 == dimension)
		{
			return GL_TEXTURE_1D;
		}
		else if (2 == dimension)
		{
			return multisample ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D;
		}
		else if (3 == dimension)
		{
			return GL_TEXTURE_3D;
		}
		else
		{
			SE_ASSERT(false, "Unkown texture dimension!");
			return GL_NONE;
		}
	}

	OpenGLTextureBuffer::OpenGLTextureBuffer(
		const U32					dimension,
		const U32*					size,
		const RenderInternalFormat	format,
		const WrapMode				wrapMode,
		const MinFilterMode			minFilterMode,
		const MagFilterMode			magFilterMode,
		const Boolean				genMipmaps,
		const Boolean				multisample)
		: mRenderID(0),
		  mInternalFormat(format), mDataFormat(ToDataFormat(format)),
		  mWrapMode(wrapMode), mMinFilterMode(minFilterMode), mMagFilterMode(magFilterMode),
		  mGenMipmaps(genMipmaps), mMultisample(multisample)
	{
		SE_PROFILE_FUNCTION();

		SE_ASSERT(dimension >= 1 || dimension <= 3, "Unkown buffer dimension!");

		for (U32 i = 0; i < dimension; ++i)
		{
			SE_ASSERT(size[i] >= 0 && size[i] <= MaxBufferSize, "Buffer size out of range!");
			mSize.push_back(size[i]);
		}

		auto textureTarget = ToGLTextureTarget(dimension, mMultisample);
		GLCALL(glCreateTextures(textureTarget, 1, &mRenderID));
		GLCALL(glBindTexture(textureTarget, mRenderID));

		// Create texture buffer.
		if (1 == dimension)
		{
			GLCALL(glTextureStorage1D(mRenderID, 1, ToGLenum(mInternalFormat), mSize[0]));
		}
		else if (2 == dimension)
		{
			GLCALL(glTextureStorage2D(mRenderID, 1, ToGLenum(mInternalFormat), mSize[0], mSize[1]));
		}
		else if (3 == dimension)
		{
			GLCALL(glTextureStorage3D(mRenderID, 1, ToGLenum(mInternalFormat), mSize[0], mSize[1], mSize[2]));
		}

		// Set texture buffer parameters.
		GLCALL(glTextureParameteri(mRenderID, GL_TEXTURE_WRAP_S, ToGLenum(mWrapMode)));
		GLCALL(glTextureParameteri(mRenderID, GL_TEXTURE_WRAP_T, ToGLenum(mWrapMode)));
		GLCALL(glTextureParameteri(mRenderID, GL_TEXTURE_WRAP_R, ToGLenum(mWrapMode)));

		GLCALL(glTextureParameteri(mRenderID, GL_TEXTURE_MIN_FILTER, ToGLenum(mMinFilterMode)));
		GLCALL(glTextureParameteri(mRenderID, GL_TEXTURE_MAG_FILTER, ToGLenum(mMagFilterMode)));

		if (mGenMipmaps)
		{
			GLCALL(glGenerateMipmap(textureTarget));
		}
		GLCALL(glBindTexture(textureTarget, 0));
	}

	OpenGLTextureBuffer::~OpenGLTextureBuffer()
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glDeleteTextures(1, &mRenderID));
	}

	U32 OpenGLTextureBuffer::getBufferSize(const U32 dimension) const
	{
		SE_PROFILE_FUNCTION();
		
		SE_ASSERT(dimension >= 1 || dimension <= 3, "Unkown texture dimension!");

		return mSize[dimension];
	}

	U32 OpenGLTextureBuffer::getRenderID() const
	{
		return mRenderID;
	}

	U32 OpenGLTextureBuffer::getDimension() const
	{
		return U32(mSize.size());
	}

	void OpenGLTextureBuffer::setBuffer(void* buffer, U32* size, U32 dimension)
	{
		SE_PROFILE_FUNCTION();

		SE_ASSERT(dimension == mSize.size(), "Buffer dimension mismatchs!");
		for (U32 i = 0; i < dimension; ++i)
		{
			SE_ASSERT(size[i] >= 0 && size[i] <= mSize[i], "Buffer size out of range!");
		}

		if (buffer)
		{
			if (1 == dimension)
			{
				GLCALL(glTextureSubImage1D(mRenderID, 0, 0, size[0], ToGLenum(mDataFormat), GL_UNSIGNED_BYTE, buffer));
			}
			else if (2 == dimension)
			{
				GLCALL(glTextureSubImage2D(mRenderID, 0, 0, 0, size[0], size[1], ToGLenum(mDataFormat), GL_UNSIGNED_BYTE, buffer));
			}
			else if (3 == dimension)
			{
				GLCALL(glTextureSubImage3D(mRenderID, 0, 0, 0, 0, size[0], size[1], size[2], ToGLenum(mDataFormat), GL_UNSIGNED_BYTE, buffer));
			}
		}
	}

	void OpenGLTextureBuffer::bind(const U32 slot) const
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glBindTextureUnit(slot, mRenderID));
	}

	//---------------------------------------------------------------------------------------------------------------------------
	// RenderBuffer
	//---------------------------------------------------------------------------------------------------------------------------

	OpenGLRenderBuffer::OpenGLRenderBuffer(
		const U32					dimension,
		const U32*					size,
		const RenderInternalFormat	format)
		: mRenderID(0),
		  mInternalFormat(format)
	{
		SE_PROFILE_FUNCTION();

		SE_ASSERT(dimension == 2, "Unkown buffer dimension!");

		for (U32 i = 0; i < dimension; ++i)
		{
			SE_ASSERT(size[i] >= 0 && size[i] <= MaxBufferSize, "Buffer size out of range!");
			mSize.push_back(size[i]);
		}

		GLCALL(glGenRenderbuffers(1, &mRenderID));
		GLCALL(glBindRenderbuffer(GL_RENDERBUFFER, mRenderID));
		GLCALL(glRenderbufferStorage(GL_RENDERBUFFER, ToGLenum(mInternalFormat), mSize[0], mSize[1]));
	}
	
	OpenGLRenderBuffer::~OpenGLRenderBuffer()
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glDeleteRenderbuffers(1, &mRenderID));
	}

	U32 OpenGLRenderBuffer::getRenderID() const
	{
		return mRenderID;
	}

	U32 OpenGLRenderBuffer::getDimension() const
	{
		return U32(mSize.size());
	}
	
	U32	OpenGLRenderBuffer::getBufferSize(const U32 dimension) const
	{
		SE_PROFILE_FUNCTION();

		SE_ASSERT(dimension >= 1 && dimension <= U32(mSize.size()), "Buffer dimension out of range!");

		return mSize[dimension];
	}

	void OpenGLRenderBuffer::setBuffer(void* buffer, U32* size, U32 dimension)
	{
	}

	void OpenGLRenderBuffer::bind()
	{
		SE_PROFILE_FUNCTION();

		GLCALL(glBindRenderbuffer(GL_RENDERBUFFER, mRenderID));
	}

	//---------------------------------------------------------------------------------------------------------------------------
	// FrameBuffer
	//---------------------------------------------------------------------------------------------------------------------------

	OpenGLFrameBuffer::OpenGLFrameBuffer(const std::initializer_list<AttachmentLayoutElement>& layout)
		: mLayout(layout),
		  mWidth(0), mHeight(0),
		  mRenderID(0)
	{
	}

	OpenGLFrameBuffer::~OpenGLFrameBuffer()
	{
		SE_PROFILE_FUNCTION();
	
		for (auto& element : mLayout)
		{
			element.buffer.reset();
		}
		GLCALL(glDeleteFramebuffers(1, &mRenderID));
	}

	void OpenGLFrameBuffer::bind() const
	{
		SE_PROFILE_FUNCTION();

		RenderAPI::SetViewport(0, 0, mWidth, mHeight);
		GLCALL(glBindFramebuffer(GL_FRAMEBUFFER, mRenderID));
	}

	void OpenGLFrameBuffer::unbind() const
	{
		SE_PROFILE_FUNCTION();

		auto windowFrameSize = WindowAPI::GetWindowSize();
		RenderAPI::SetViewport(0, 0, windowFrameSize.x, windowFrameSize.y);
		GLCALL(glBindFramebuffer(GL_FRAMEBUFFER, 0));
	}

	U32 OpenGLFrameBuffer::getWidth() const
	{
		return mWidth;
	}

	U32 OpenGLFrameBuffer::getHeight() const
	{
		return mHeight;
	}

	U32 OpenGLFrameBuffer::getAttachmentID(const AttachmentType type) const
	{
		return mLocationCaches.at(type);
	}

	void OpenGLFrameBuffer::setBuffer(const U32 width, const U32 height)
	{
		SE_PROFILE_FUNCTION();

		if (width > MaxBufferSize || height > MaxBufferSize ||
			width == 0 || height == 0 ||
			(width == mWidth && height == mHeight))
			return;

		mWidth = width;
		mHeight = height;

		if (mRenderID)
		{
			for (auto& element : mLayout)
			{
				element.buffer.reset();
			}

			GLCALL(glDeleteFramebuffers(1, &mRenderID));

			mLocationCaches.clear();
		}

		GLCALL(glGenFramebuffers(1, &mRenderID));
		GLCALL(glBindFramebuffer(GL_FRAMEBUFFER, mRenderID));

		U32 size[] = { mWidth, mHeight };
		for (auto& element : mLayout)
		{
			switch (element.attachmentBufferType)
			{
				default: break;
				case AttachmentBufferType::Texture:
				{
					element.buffer = TextureBuffer::Create(
						2,
						size,
						element.internalFormat,
						TextureBuffer::WrapMode::Repeat,
						TextureBuffer::MinFilterMode::Linear,
						TextureBuffer::MagFilterMode::Linear,
						false,
						false
					);
					mLocationCaches[element.attachmentType] = element.buffer->getRenderID();
					GLCALL(glFramebufferTexture2D(GL_FRAMEBUFFER, ToGLenum(element.attachmentType), ToGLenum(element.attachmentBufferType), element.buffer->getRenderID(), 0));
					break;
				}
				case AttachmentBufferType::RenderBufferObject:
				{
					element.buffer = RenderBuffer::Create(
						2,
						size,
						element.internalFormat
					);
					mLocationCaches[element.attachmentType] = element.buffer->getRenderID();
					GLCALL(glFramebufferRenderbuffer(GL_FRAMEBUFFER, ToGLenum(element.attachmentType), ToGLenum(element.attachmentBufferType), element.buffer->getRenderID()));
					break;
				}
			}
		}

		// Bind render target.
		if (0 == mLocationCaches[AttachmentType::Color] && 0 != mLocationCaches[AttachmentType::Depth])
		{
			GLCALL(glDrawBuffer(GL_NONE));
		}
		if (0 != mLocationCaches[AttachmentType::Color])
		{
			GLCALL(glDrawBuffer(GL_COLOR_ATTACHMENT0));
		}

		// Check.
		GLCALL(auto success = glCheckFramebufferStatus(GL_FRAMEBUFFER));
		SE_ASSERT(GL_FRAMEBUFFER_COMPLETE == success, "Framebuffer is incomplete!");

		GLCALL(glBindFramebuffer(GL_FRAMEBUFFER, 0));
	}
} // !namespace SE